深入讲解Android Property机制 |
您所在的位置:网站首页 › load properties › 深入讲解Android Property机制 |
深入讲解Android Property机制 侯亮 1 概述Android系统(本文以Android 4.4为准)的属性(Property)机制有点儿类似Windows系统的注册表,其中的每个属性被组织成简单的键值对(key/value)供外界使用。 我们可以通过在adb shell里敲入getprop命令来获取当前系统的所有属性内容,而且,我们还可以敲入类似“getprop 属性名”的命令来获取特定属性的值。另外,设置属性值的方法也很简单,只需敲入“setprop 属性名 新值”命令即可。 可是问题在于我们不想只认识到这个层次,我们希望了解更多一些Property机制的运作机理,而这才是本文关心的重点。
说白了,Property机制的运作机理可以汇总成以下几句话:1) 系统一启动就会从若干属性脚本文件中加载属性内容;2) 系统中的所有属性(key/value)会存入同一块共享内存中;3) 系统中的各个进程会将这块共享内存映射到自己的内存空间,这样就可以直接读取属性内容了;4) 系统中只有一个实体可以设置、修改属性值,它就是属性服务(Property Service);5) 不同进程只可以通过socket方式,向属性服务发出修改属性值的请求,而不能直接修改属性值;6) 共享内存中的键值内容会以一种字典树的形式进行组织。
Property机制的示意图如下:
2 Property Service 2.1 init进程里的Property Service Property Service实体其实是在init进程里启动的。我们知道,init是Linux系统中用户空间的第一个进程。它负责创建系统中最关键的几个子进程,比如zygote等等。在本节中,我们主要关心init进程是如何启动Property Service的。
我们查看core/init/Init.c文件,可以看到init进程的main()函数,它里面和property相关的关键动作有:1)间接调用__system_property_area_init():打开属性共享内存,并记入__system_property_area变量;2)间接调用init_workspace():只读打开属性共享内存,并记入环境变量;3)根据init.rc,异步激发property_service_init_action(),该函数中会: l 加载若干属性文本文件,将具体属性、属性值记入属性共享内存; l 创建并监听socket;4)根据init.rc,异步激发queue_property_triggers_action(),将刚刚加载的属性对应的激发动作,推入action列表。
main()中的调用关系如下:
2.1.1 初始化属性共享内存 我们可以看到,在init进程的main()函数里,辗转打开了一个内存文件“/dev/__properties__”,并把它设定为128KB大小,接着调用mmap()将这块内存映射到init进程空间了。这个内存的首地址被记录在__system_property_area__全局变量里,以后每添加或修改一个属性,都会基于这个__system_property_area__变量来计算位置。
初始化属性内存块时,为什么要两次open那个/dev/__properties__文件呢?我想原因是这样的:第一次open的句柄,最终是给属性服务自己用的,所以需要有读写权限;而第二次open的句柄,会被记入pa_workspace.fd,并在合适时机添加进环境变量,供其他进程使用,因此只能具有读取权限。
第一次open时,执行的代码如下: fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444); 传给open()的参数标识里指明了O_RDWR,表示用“读写方式”打开文件。另外O_NOFOLLOW标识主要是为了防止我们打开“符号链接”,不过我们知道,__properties__文件并不是符号链接,所以当然可以成功open。O_CLOEXEC标识是为了保证一种独占性,也就是说当init进程打开这个文件时,此时就算其他进程也open这个文件,也会在调用exec执行新程序时自动关闭该文件句柄。O_EXCL标识和O_CREATE标识配合起来,表示如果文件不存在,则创建之,而如果文件已经存在,那么open就会失败。第一次open动作后,会给__system_property_area__赋值,然后程序会立即close刚打开的句柄。
第二次open动作发生在接下来的init_workspace()函数里。此时会再一次打开__properties__文件,这次却是以只读模式打开的: int fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW); 打开的句柄记录在pa_workspace.fd处,以后每当init进程执行socket命令,并调用service_start()时,会执行类似下面的句子: [cpp] view plain copy get_property_workspace(&fd, &sz); // 读取pa_workspace.fd sprintf(tmp, "%d,%d", dup(fd), sz); add_environment("ANDROID_PROPERTY_WORKSPACE", tmp); 说白了就是把 pa_workspace.fd 的句柄记入一个名叫“ ANDROID_PROPERTY_WORKSPACE ”的环境变量去。
【system/core/init/Init.c】 [cpp] view plain copy /* add_environment - add "key=value" to the current environment */ int add_environment(const char *key, const char *val) { int n; for (n = 0; n PROP_NAME_MAX) { ERROR("property name too long in trigger %s", act->name); } else { memcpy(prop_name, name, length); prop_name[length] = 0; /* does the property exist, and match the trigger value? */ property_get(prop_name, value); if (!strcmp(equals + 1, value) ||!strcmp(equals + 1, "*")) { action_add_queue_tail(act); } } } } } }这段代码是说,当获取的属性名和属性值,与当初init.rc里记录的某action的激发条件匹配时,就把该action插入执行队列的尾部(action_add_queue_tail(act))。 2.2 init进程循环监听socket 现在再回过头看init进程,其main()函数的最后,我们可以看到一个for(;;)循环,不断监听外界发来的命令,包括设置属性的命令。 【system/core/init/Init.c】 [cpp] view plain copy for(;;) { . . . . . . . . . . . . nr = poll(ufds, fd_count, timeout); if (nr name, ei->value); . . . . . . 其中 get_property_workspace() 的代码如下: [cpp] view plain copy void get_property_workspace(int *fd, int *sz) { *fd = pa_workspace.fd; *sz = pa_workspace.size; }大家还记得前文阐述init_workspace()时,把打开的句柄记入pa_workspace.fd的句子吧,现在就是在用这个句柄。
一切准备好后,service_start()会调用execve(),执行svc->args[0]所指定的可执行文件,然后还要再写个属性值: [cpp] view plain copy void service_start(struct service *svc, const char *dynamic_args) { . . . . . . . . . . . . execve(svc->args[0], (char**) arg_ptrs, (char**) ENV); . . . . . . . . . . . . svc->time_started = gettime(); svc->pid = pid; svc->flags |= SVC_RUNNING; if (properties_inited()) notify_service_state(svc->name, "running"); } 其中的notify_service_state()的代码如下: [cpp] view plain copy void notify_service_state(const char *name, const char *state) { char pname[PROP_NAME_MAX]; int len = strlen(name); if ((len + 10) > PROP_NAME_MAX) return; snprintf(pname, sizeof(pname), "init.svc.%s", name); property_set(pname, state); }一般情况下,这种在init.rc里记录的系统service的名字都不会超过22个字节,加上“init.svc.”前缀也不会超过31个字节,所以每次启动service,都会修改相应的属性。比如netd服务,一旦它被启动,就会将init.svc.netd属性的值设为“running”。 以上是handle_control_message()处理“ctl.start”命令时的情况,相应地还有处理“ctl.stop”命令的情况,此时会调用到msg_stop()。 【system/core/init/Init.c】 [cpp] view plain copy static void msg_stop(const char *name) { struct service *svc = service_find_by_name(name); if (svc) { service_stop(svc); } else { ERROR("no such service '%s'\n", name); } }[cpp] view plain copy void service_stop(struct service *svc) { service_stop_or_reset(svc, SVC_DISABLED); } [cpp] view plain copy static void service_stop_or_reset(struct service *svc, int how) { /* The service is still SVC_RUNNING until its process exits, but if it has * already exited it shoudn't attempt a restart yet. */ svc->flags &= (~SVC_RESTARTING); if ((how != SVC_DISABLED) && (how != SVC_RESET) && (how != SVC_RESTART)) { /* Hrm, an illegal flag. Default to SVC_DISABLED */ how = SVC_DISABLED; } /* if the service has not yet started, prevent * it from auto-starting with its class */ if (how == SVC_RESET) { svc->flags |= (svc->flags & SVC_RC_DISABLED) ? SVC_DISABLED : SVC_RESET; } else { svc->flags |= how; } if (svc->pid) { NOTICE("service '%s' is being killed\n", svc->name); kill(-svc->pid, SIGKILL); notify_service_state(svc->name, "stopping"); } else { notify_service_state(svc->name, "stopped"); } } 可以看到,停止一个service时,主要是调用kill( )来杀死服务子进程,并将init.svc.xxx属性值设为stopping。 OK,终于把init进程里,处理“ctl.”命令的部分讲完了,下面我们接着看init进程处理普通属性的部分。 2.2.2 处理属性设置命令 我们还是先回到前文init进程处理属性设置动作的地方: [cpp] view plain copy void handle_property_set_fd() { . . . . . . if(memcmp(msg.name,"ctl.",4) == 0) { . . . . . . } else { if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) { property_set((char*) msg.name, (char*) msg.value); } else { ERROR("sys_prop: permission denied uid:%d name:%s\n", cr.uid, msg.name); } . . . . . . close(s); } . . . . . . break; . . . . . . } }2.2.2.1 check_perms() 要设置普通属性,也是要具有一定权限哩。请看上面的 check_perms() 一句。该函数的代码如下: [cpp] view plain copy static int check_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx) { int i; unsigned int app_id; if(!strncmp(name, "ro.", 3)) name +=3; if (uid == 0) return check_mac_perms(name, sctx); app_id = multiuser_get_app_id(uid); if (app_id == AID_BLUETOOTH) { uid = app_id; } for (i = 0; property_perms[i].prefix; i++) { if (strncmp(property_perms[i].prefix, name, strlen(property_perms[i].prefix)) == 0) { if ((uid && property_perms[i].uid == uid) || (gid && property_perms[i].gid == gid)) { return check_mac_perms(name, sctx); } } } return 0; } 主要也是在查表,property_perms表的定义如下: 这其实很容易理解,比如要设置“sys.”打头的系统属性,进程的uid就必须是AID_SYSTEM,否则阿猫阿狗都能设置系统属性,岂不糟糕。 2.2.2.2 property_set()权限检查通过之后,就可以真正设置属性了。在前文“概述”一节中,我们已经说过,只有Property Service(即init进程)可以写入属性值,而普通进程最多只能通过socket向Property Service发出设置新属性值的请求,最终还得靠Property Service来写。那么我们就来看看Property Service里具体是怎么写的。 总体说来,property_set()会做如下工作: 1) 判断待设置的属性名是否合法; 2) 尽力从“属性共享内存”中找到匹配的prop_info节点,如果能找到,就调用__system_property_update(),当然如果属性是以“ro.”打头的,说明这是个只读属性,此时不会update的;如果找不到,则调用__system_property_add()添加属性节点。 3) 在update或add动作之后,还需要做一些善后处理。比如,如果改动的是“net.”开头的属性,那么需要重新设置一下net.change属性,属性值为刚刚设置的属性名字。 4) 如果要设置persist属性的话,只有在系统将所有的默认persist属性都加载完毕后,才能设置成功。persist属性应该是那种会存入可持久化文件的属性,这样,系统在下次启动后,可以将该属性的初始值设置为系统上次关闭时的值。5) 如果将“selinux.reload_policy”属性设为“1”了,那么会进一步调用selinux_reload_policy()。这个意味着要重新加载SEAndroid策略。 6) 最后还需调用property_changed()函数,其内部会执行init.rc中指定的那些和property同名的action。 【core/init/Property_service.c】 [cpp] view plain copy int property_set(const char *name, const char *value) { . . . . . . . . . . . . pi = (prop_info*) __system_property_find(name); if(pi != 0) { if(!strncmp(name, "ro.", 3)) return -1; __system_property_update(pi, value, valuelen); } else { ret = __system_property_add(name, namelen, value, valuelen); . . . . . . } if (strncmp("net.", name, strlen("net.")) == 0) { if (strcmp("net.change", name) == 0) { return 0; } property_set("net.change", name); } else if (persistent_properties_loaded && strncmp("persist.", name, strlen("persist.")) == 0) { write_persistent_property(name, value); } else if (strcmp("selinux.reload_policy", name) == 0 && strcmp("1", value) == 0) { selinux_reload_policy(); } property_changed(name, value); return 0; }一开始当然要先找到“希望设置的目标属性”在共享内存里对应的prop_info节点啦,后续关于__system_property_update()和__system_property_add()的操作,主要都是在操作该prop_info节点,代码比较简单。prop_info的详细内容我们会在下文阐述,这里先跳过。 如果可以找到prop_info节点,就尽量将这个属性的值更新一下,除非是遇到“ro.”属性,这种属性是只读的,当然不能set。如果找不到prop_info节点,此时会为这个新属性创建若干字典树节点,包括最终的prop_info叶子。 属性写入完毕后,还要调用property_changed(),做一些善后处理: 【system/core/init/Init.c】 [cpp] view plain copy void property_changed(const char *name, const char *value) { if (property_triggers_enabled) queue_property_triggers(name, value); }【 system/core/init/Init_parser.c 】 [cpp] view plain copy void queue_property_triggers(const char *name, const char *value) { struct listnode *node; struct action *act; list_for_each(node, &action_list) { act = node_to_item(node, struct action, alist); if (!strncmp(act->name, "property:", strlen("property:"))) { const char *test = act->name + strlen("property:"); int name_length = strlen(name); if (!strncmp(name, test, name_length) && test[name_length] == '=' && (!strcmp(test + name_length + 1, value) || !strcmp(test + name_length + 1, "*"))) { action_add_queue_tail(act); } } } }[cpp] view plain copy void action_add_queue_tail(struct action *act) { if (list_empty(&act->qlist)) { list_add_tail(&action_queue, &act->qlist); } } 从代码可以看出,当某个属性修改之后, Property Service 会遍历一遍 action_list 列表,找到其中匹配的 action 节点,并将之添加进 action_queue 队列。之所以会有 if (list_empty(&act->qlist)) 判断,是为了防止重复添加。下面是 init.rc 脚本中的一个片段: 【system/core/rootdir/init.rc】 这几个就是和property相关的action,其他相关的action还有不少,我们就不列了。我们以第一个action为例来说明。如果我们修改了vold.decrypt属性的值,那么queue_property_triggers()搜索action_list时,就能找到一个名为“property:vold.decrypt=trigger_reset_main”的action节点,此时的逻辑无非是比较“冒号后的名字”、“赋值号后的值”,是否分别和queue_property_triggers()的name、value参数匹配,如果匹配,就把这个action节点添加进action_queue队列里。 3 客户进程访问属性的机制 3.1 映射“属性共享内存”的时机现在有一个问题必须先提出来,那就是“属性共享内存”是在什么时刻映射进用户进程空间的?总不会平白无故地就可以成功调用property_get()吧。其实,为了让大家方便地调用property_get(),属性机制的设计者的确是用了一点儿小技巧,下面我们就来看看细节。 3.1.1 静态加载时的初始化在前文介绍Init进程初始化属性共享内存时,调用了一个叫做__system_property_area_init()的函数: 【bionic/libc/bionic/System_properties.c】 [cpp] view plain copy int __system_property_area_init() { return map_prop_area_rw(); }它映射时需要的是读写权限。而对普通进程而言,只有读权限,当然不可能调用__system_property_area_init()了。其实在System_properties.c文件中,我们还可以找到另一个长得挺像的初始化函数——__system_properties_init(): [cpp] view plain copy int __system_properties_init() { return map_prop_area(); }它调用的map_prop_area()会把属性共享内存,以只读模式映射到用户进程空间: [cpp] view plain copy static int map_prop_area() { fd = open(property_filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); . . . . . . if ((fd preinit_array); call_array(structors->init_array); . . . . . . exit(slingshot(args.argc, args.argv, args.envp)); }当一个用户进程被调用起来时,内核会先调用到C运行期库(crtbegin)层次来初始化运行期环境,在这个阶段就会调用到__libc_init(),而后才会间接调用到C程序员熟悉的main()函数。可见属性共享内存在执行main()函数之前就已经映射好了。 3.1.2 动态加载时的初始化除了__libc_init()中会调用__libc_init_common(),还有一处会调用。 【bionic/libc/bionic/Libc_init_dynamic.cpp】 [cpp] view plain copy __attribute__((constructor)) static void __libc_preinit() { . . . . . . __libc_init_common(*args); . . . . . . pthread_debug_init(); malloc_debug_init(); }请大家注意函数名那一行起始处的__attribute__((constructor))属性,这是GCC的一个特有属性。被这种属性修饰的函数会被放置在特殊的代码段中。这样,当动态链接器一加载libc.so时,会尽早执行__libc_preinit()函数。这样一来,动态库里也可以放心调用property_get()了。 3.2 读取属性值 下面我们来集中精力研究读取属性值的部分。我们在前文留下过一个尾巴,当时对属性共享内存块里的prop_info节点,只做了非常简略的提及,现在我们就来细说它。
说白了,属性共享内存中的内容,其实被组织成一棵字典树。内存块的第一个节点是个特殊的总述节点,类型为prop_area。紧随其后的就是字典树的“树枝”和“树叶”了,树枝以prop_bt表达,树叶以prop_info表达。我们读取或设置属性值时,最终都只是在操作“叶子”节点而已。 3.2.1 “属性共享内存”里的数据结构
【bionic/libc/bionic/System_properties.c】 [cpp] view plain copy struct prop_area { unsigned bytes_used; unsigned volatile serial; unsigned magic; unsigned version; unsigned reserved[28]; char data[0]; }; typedef struct prop_area prop_area; struct prop_info { unsigned volatile serial; char value[PROP_VALUE_MAX]; char name[0]; }; typedef struct prop_info prop_info;[cpp] view plain copy typedef volatile uint32_t prop_off_t; struct prop_bt { uint8_t namelen; uint8_t reserved[3]; prop_off_t prop; prop_off_t left; prop_off_t right; prop_off_t children; char name[0]; }; typedef struct prop_bt prop_bt;
现在的问题是,这棵树是如何组织其枝叶的?System_properties.c文件中,有一段注释,给出了一个不算太清楚的示意图,截取如下:
看过这张图后,各位同学搞清楚了吗?反正我一开始没有搞清楚,后来只好研究代码,现在算是知道一点儿了,详情如下: l 一开始的prop_area节点严格地说并不属于字典树,但是它代表着属性共享内存块的起始; l 紧接着prop_area节点,需要有一个空白的prop_bt节点。这个是必须的噢,在前文说明init进程的main()函数的调用关系图中,我们表达了这个概念:这个就是空节点; l 属性名将以‘.’符号为分割符,被分割开来。比如ro.secure属性名就会被分割成“ro”和“secure”两部分,而且每个部分用一个prop_bt节点表达。 l 属性名中的这种‘.’关系被表示为父子关系,所以“ro”节点的children域,会指向“secure”节点。但是请注意,一个节点只有一个children域,如果它还有其他孩子,那些孩子将会和第一个子节点(比如secure节点)组成一棵二叉树。 l 当一个属性名对应的“字典树枝”都已经形成好后,会另外创建一个prop_info节点,专门表示这个属性,该节点就是“字典树叶”。
下面我们画几张图来说明问题。比如我们现在手头有3个属性,分别为 ro.abc.def ro.hhh.def sys.os.ccc 我们依此顺序设置属性,就会形成下面这样的树:
其中天蓝色块表示prop_area节点,桔黄色块表示prop_bt节点,浅绿色块表示prop_info节点。简单地说,父节点的children域,只指代其第一个子节点。后续从属于同一父节点的兄弟子节点,会被组织成一棵二叉子树,该二叉子树的根就是父节点的第一个子节点。我们用蓝色箭头来表示二叉子树的关系,在代码中对应prop_bt的left、right域。这么说来,以不同顺序添加属性,其实会导致最终得到的字典树在形态上发生些许变化。
prop_bt节点的name域只记录“树枝”的名字,比如“ro”、“abc”、“def”等等,而prop_info节点的name域记录的则是属性的全名,比如“ro.abc.def”。
现在我们向上面这棵字典树中再添加一个rs.ppp.qqq属性,会形成如下字典树: “rs”节点之所以在那个位置,是基于strcmp()的计算结果。“rs”字符串比“ro”字符串大,所以进一步和“ro”的right节点(即“sys”节点)比对,“rs”又比“sys”小,所以在“sys”节点的left枝上建立了新节点。
以上是画成字典树的样子,它表示的是一种逻辑关系。而在实际的“属性共享内存”中,这些节点基本上是紧凑排列的,大体上会形成下面这样的排列关系:
说到这里,大家应该已经比较清楚属性共享内存块是怎么组织的吧。有了这种大致思路,再去看相应的代码,相信大家会轻松一点儿。 3.2.2 property_get() 在读取具体属性值时,最终会调用到property_get()函数,该函数的调用关系如下: 说白了就是先从字典树中找到感兴趣的prop_info叶子,然后把叶子里的值读出来。 4 Java层的封装接下来我们再说说属性机制里Java层的封装。这部分比较简单,因为它主要只是在简单包装C语言层次的函数。
Java层使用的属性机制被封装在SystemProperties中: 【frameworks/base/core/java/android/os/SystemProperties.java】 [java] view plain copy public class SystemProperties { public static final int PROP_NAME_MAX = 31; public static final int PROP_VALUE_MAX = 91; private static final ArrayList sChangeCallbacks = new ArrayList(); private static native String native_get(String key); private static native String native_get(String key, String def); private static native int native_get_int(String key, int def); private static native long native_get_long(String key, long def); private static native boolean native_get_boolean(String key, boolean def); private static native void native_set(String key, String def); private static native void native_add_change_callback(); /** * Get the value for the given key. * @return an empty string if the key isn't found * @throws IllegalArgumentException if the key exceeds 32 characters */ public static String get(String key) { if (key.length() > PROP_NAME_MAX) { throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); } return native_get(key); } . . . . . . . . . . . .
我们就以上面的get()成员函数为例来说明,它基本上只是在调用native_get()函数而已,该函数对应的C语言函数可以从下表查到,就是那个SystemProperties_getS(): 【frameworks/base/core/jni/android_os_SystemProperties.cpp】 [cpp] view plain copy static JNINativeMethod method_table[] = { { "native_get", "(Ljava/lang/String;)Ljava/lang/String;", (void*) SystemProperties_getS }, { "native_get", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void*) SystemProperties_getSS }, { "native_get_int", "(Ljava/lang/String;I)I", (void*) SystemProperties_get_int }, { "native_get_long", "(Ljava/lang/String;J)J", (void*) SystemProperties_get_long }, { "native_get_boolean", "(Ljava/lang/String;Z)Z", (void*) SystemProperties_get_boolean }, { "native_set", "(Ljava/lang/String;Ljava/lang/String;)V", (void*) SystemProperties_set }, { "native_add_change_callback", "()V", (void*) SystemProperties_add_change_callback }, };
【frameworks/base/core/jni/android_os_SystemProperties.cpp】 [cpp] view plain copy static jstring SystemProperties_getS(JNIEnv *env, jobject clazz, jstring keyJ) { return SystemProperties_getSS(env, clazz, keyJ, NULL); } static jstring SystemProperties_getSS(JNIEnv *env, jobject clazz, jstring keyJ, jstring defJ) { int len; const char* key; char buf[PROPERTY_VALUE_MAX]; jstring rvJ = NULL; if (keyJ == NULL) { jniThrowNullPointerException(env, "key must not be null."); goto error; } key = env->GetStringUTFChars(keyJ, NULL); len = property_get(key, buf, ""); if ((len = 0) { rvJ = env->NewStringUTF(buf); } else { rvJ = env->NewStringUTF(""); } env->ReleaseStringUTFChars(keyJ, key); error: return rvJ; }最终调用的还是property_get()函数。 5 尾声至此,有关Android属性机制的大体机理就讲解完毕了,希望对大家有点儿帮助。 转自http://blog.csdn.net/codefly/article/details/48379239 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |